--- title: "03-C++ 核心" created: 2025-11-25 tags: - 项目筑基 --- # C++ 核心 > 内存分区模型、引用、函数重载、类和对象(封装/继承/多态)、文件操作,收尾于职工管理系统实战。 面向对象 ### 内存分区模型 C++程序在执行时,将内存大方向划分为4个区域 代码区:存放函数体的二进制代码,由操作系统进行管理的 全局区:存放全局变量和静态变量以及常量 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收 内存四区意义: 不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程 程序运行前(编译生成exe文件 但未双击执行) 就已经有了代码区和全局区 #### 代码区: 存放 CPU 执行的机器指令(二进制的0101) 特点: 共享:对于频繁被执行的程序,只需要在内存中有一份代码即可 只读:使其只读的原因是防止程序意外地修改了它的指令 (能同时打开多个VS[双开] 但真正的代码只有一份 不会另外生成一份同样的代码 这叫做共享 只读就是不能在程序执行时修改源代码[防开挂咯]) #### 全局区: 全局变量和静态变量存放在此. 全局区还包含了常量区, 字符串常量和全局常量也存放在此. (对于全局区包含常量区的说法,在某些情况下是不准确的,而在其他情况下是正确的,具体取决于编译器和平台的实现。 在C++中,全局区(或数据段)通常包含了全局变量和静态变量。全局变量是在程序启动时创建,并在程序结束时销毁的变量。静态变量则具有更长的生命周期,跨越多个函数调用。 常量区(或常量存储区)是存储常量数据的一部分,如字符串常量和其他编译时确定的常量。这些数据通常被放置在只读存储区域,以防止其被修改。 在一些编译器和平台的实现中,全局区和常量区可能会合并在一起,因为它们都属于静态存储区域。这样做可以优化内存布局,使得常量数据和全局变量一起存储,从而减少内存的碎片化。在这种情况下,全局区可以包含常量区。 然而,也有一些编译器和平台会将全局区和常量区分开。这样做的目的是明确区分可修改的全局变量和不可修改的常量数据,从而提高程序的可读性和安全性。 因此,总结来说,全局区是否包含常量区取决于具体的编译器和平台实现。在某些情况下,它们可能被合并在一起,而在其他情况下,它们可能被分开存储。) 该区域的数据在程序结束后由操作系统释放 ![[cpp-image29.png]] 顾名思义——局部的局部变量和局部常量不在全局区 除局部常量外 字符串常量、全局常量都和全局变量跟静态本地变量存放在同一块地方——全局区 ![[cpp-image30.png]] C++中在程序运行前分为全局区和代码区 代码区特点是共享和只读 全局区中存放全局变量、静态变量、常量 常量区中存放 const修饰的全局常量 和 字符串常量 在程序运行后 才会出现栈区和堆区 #### 栈区: 由编译器自动分配释放, 存放函数的参数值,局部变量等 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放 (函数里的变量 在离开函数后 那块地址里面的东西是不受控制的 就像租房到期离开 这块地址又存放了其他东西 原本的数据去哪了我们并不知道也没必要关心 所以传局部变量的地址是没有意义的 里面的内容不由我们控制) ![[cpp-image31.png]] 这块地址又“租”给了function_2里的变量b 但这是不确定的 这块地址到底给谁用 我们并不知道 这是编译器做的事 我们只知道 对于局部变量 返回地址是没有意义的 函数的调用和退出机制与数据结构中栈的先进后出(LIFO)特性是一致的,因此函数的调用和退出通常会使用栈来进行内存分配和管理。 具体地说,函数调用时会在栈上创建一个函数栈帧,用于存储函数的局部变量、函数参数和其他相关信息。函数栈帧包含返回地址、参数和局部变量、上一个函数的栈帧指针以及其他保存的寄存器值。栈帧的创建和销毁遵循栈的先进后出原则。 通过栈帧,程序可以轻松地管理函数调用和退出的状态。每个函数都有自己的栈帧,它们按照先进后出的顺序在栈上进行管理。栈帧的使用使得函数的嵌套调用和递归成为可能,并提供了一种跟踪函数执行状态的机制。 函数栈帧的使用也有其局限性,栈的大小是有限的,递归过深或函数调用过于频繁可能导致栈溢出的问题。当栈空间不足以容纳新的栈帧时,通常会导致程序异常终止。 总而言之,函数栈帧是函数调用时在栈上分配的内存区域,通过利用栈的先进后出特性,实现了函数调用和退出的管理。它的使用使得函数的嵌套调用和递归成为可能,并提供了一种方便的方式来跟踪函数的执行状态。但需要注意栈的大小有限,递归过深或函数调用过于频繁可能导致栈溢出的问题。 #### 堆区: 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收 在C++中主要利用new在堆区开辟内存 delete释放内存 (c中是malloc和free) ```cpp int *p=new int; ``` new一块int类型大小的空间 用p指向 指针p本质还是局部变量 在栈区 而它所保存的数据在堆区 (指针是局部变量,指向堆区,因返回的是堆区的地址,解引用后是堆区的数据) new的返回值是指针 所以要用相应的指针类型去接收申请到的空间的地址 new的用法: ```cpp int* p = new int(10); ``` 申请的同时赋上初值 相当于 ```cpp int* p = new int; *p = 10; cout << *p << endl; ``` delete一般就跟保存它的变量名字 比如这里用p保存的地址 所以就delete p; 也可以用其他指针 去指向这一块地方 delete那个指向该地方的指针就行 如果已经释放 再去解引用的话就会报错了(释放的空间不可访问) 对于申请来的一个数组 ```cpp int* arr = new int[10]; for (int i = 0; i < 10; i++) arr[i] = i; for (int i = 0; i < 10; i++) cout << arr[i] << endl; //释放数组 delete 后加 [] delete[] arr; ``` 要给出[]表示释放一大段数据 而非一个数据 ### 引用 作用: 给变量起别名 语法: 数据类型 &别名 = 原名 对于一块int类型的空间 ![[cpp-image32.png]] 原本使用的是用一个相应的指针类型去存放它的地址以达成访问内容的目的 引用则是对这块空间 再取一个名字 对新名字b的操作就是对a的操作 ![[cpp-image33.png]] 注意事项 引用必须初始化 ![[cpp-image34.png]] 引用在初始化后,不可以改变(int *const p) ![[cpp-image35.png]] 可以b=c; 做的是把c的值赋给b 修改的也就是a的值 它就相当于int * const p 不能改指向 但能改里面的值 可以看出就是一个传值和传址的问题 只有放在函数里去看才更能凸显意义 #### 与函数 作用:函数传参时,可以利用引用 让形参修饰实参 优点:可以简化指针修改实参 直接把该变量放入函数进行操作 所做的修改会同步于函数外的数值 也是一种传址 引用是可以作为函数的返回值存在的 注意:不要返回局部变量引用 用法:函数调用作为左值 ```cpp int& test02() { static int a = 20; return a; } test02() = 1000; ``` 修改的就是a #### 本质 引用的本质在c++内部实现是一个指针常量 &作为引用符时 就是C++对C中*的一个封装 对于 ```cpp int a=10; int &ref=a; ``` 其实相当于int *p=&a; 在之后看到对ref的操作时 会自动转换成对*ref的操作 ```cpp ref=20; ``` 其实就是*p=20; ```cpp //发现是引用,转换为 int* const ref = &a; void func(int& ref) { ref = 100; // ref是引用,转换为*ref = 100 } int main() { int a = 10; //自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改 int& ref = a; ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20; cout << "a:" << a << endl; cout << "ref:" << ref << endl; func(a); return 0; } ``` C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了 #### 与const 同样,在函数形参列表中,可以加const修饰形参,防止形参改变实参 ```cpp void showValue(const int& v) { //v += 10; cout << v << endl; ``` } 保证在该函数里不会改变外面的值 ### 函数重载 #### 函数默认参数 在C++中,函数的形参列表中的形参是可以有默认值的 语法:返回值类型 函数名 (参数= 默认值){} ```cpp int func(int a, int b = 10, int c = 10) { return a + b + c; } //1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值 //2. 如果函数声明有默认值,函数实现的时候就不能有默认参数 int func2(int a = 10, int b = 10); int func2(int a, int b) { return a + b; } int main() { cout << "ret = " << func(20, 20) << endl; cout << "ret = " << func(100) << endl; system("pause"); return 0; } ``` #### 函数占位参数 C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置 语法: 返回值类型 函数名 (数据类型){} ```cpp #include using namespace std; //函数占位参数 ,占位参数也可以有默认参数 void func(int a, int) { cout << "this is func" << endl; } int main() { func(10, 10); //占位参数必须填补 system("pause"); return 0; } ``` 可以加上默认参数实现缺省 ```cpp #include using namespace std; void func(int a, int = NULL); void func(int a, int) { cout << "this is func" << endl; } int main() { func(10); system("pause"); return 0; } ``` #### 函数重载 作用:函数名可以相同,提高复用性 函数重载满足条件: 同一个作用域下 函数名称相同 函数参数类型不同 或者 个数不同 或者 顺序不同 注意: 函数的返回值不可以作为函数重载的条件 ```cpp //函数重载需要函数都在同一个作用域下 void func(){ cout << "func 的调用!" << endl; } void func(int a){ cout << "func (int a) 的调用!" << endl; } void func(double a){ cout << "func (double a)的调用!" << endl; } void func(int a, double b){ cout << "func (int a ,double b) 的调用!" << endl; } void func(double a, int b){ cout << "func (double a ,int b)的调用!" << endl; } //函数返回值不可以作为函数重载条件 //int func(double a, int b){ //cout << "func (double a ,int b)的调用!" << endl; //} int main() { func(); func(10); func(3.14); func(10, 3.14); func(3.14, 10); return 0; } ``` 引用作为重载条件函数重载碰到函数默认参数 ```cpp //1、引用作为重载条件 void func(int& a) { cout << "func (int &a) 调用 " << endl; } void func(const int& a) { cout << "func (const int &a) 调用 " << endl; } //2、函数重载碰到函数默认参数 void func2(int a, int b = 10) { cout << "func2(int a, int b = 10) 调用" << endl; } void func2(int a) { cout << "func2(int a) 调用" << endl; } int main() { int a = 10; func(a); //调用无const func(10);//调用有const //func2(10); //碰到默认参数产生歧义,需要避免 system("pause"); return 0; } ``` ### 类和对象 C++面向对象的三大特性为:封装、继承、多态 万事万物都皆为对象 对象上有其属性和行为 例如: 人可以作为对象,属性有姓名、年龄、身高、体重...,行为有走、跑、跳、吃饭、唱歌... 车也可以作为对象,属性有轮胎、方向盘、车灯...,行为有载人、放音乐、放空调... 具有相同性质的==对象==,我们可以抽象称为==类==,人属于人类,车属于车类 #### 封装 将属性和行为作为一个整体,表现生活中的事物 将属性和行为加以权限控制 在设计类的时候,属性和行为写在一起,表现事物 语法: class 类名{ 访问权限: 属性 / 行为 }; ```cpp #include using namespace std; const double PI = 3.14; //将属性和行为作为一个整体,用来表现生活中的事物 //封装一个圆类,求圆的周长 //class代表设计一个类,后面跟着的是类名 class Circle{ public: //访问权限 公共的权限 //属性 int m_r; //半径 //行为 //获取到圆的周长 double calculateZC() { //2 * pi * r //获取圆的周长 return 2 * PI * m_r; } }; int main() { //通过圆类,创建圆的对象 // c1就是一个具体的圆 Circle c1; c1.m_r = 10; //给圆对象的半径 进行赋值操作 //2 * pi * 10 = = 62.8 cout << "圆的周长为: " << c1.calculateZC() << endl; system("pause"); return 0; } ``` 类其实就是自定义的一个数据类型 将一些需要表达意思的元素放在里面作为属性 把一些发挥作用的函数也放在里面 作为行为(方法) 属性和行为统称为成员 但这仅仅只是数据类型 没有实例化 像int i 也需要一个i来实例int类型 所以声明变量其实就是一个实例化的过程 在之前只是类 而实例化后成为生动的个体 类在设计时,可以把属性和行为放在不同的权限下,加以控制 访问权限有三种: ```cpp public 公共权限 类内可以访问 类外可以访问 protected 保护权限 类内可以访问 类外不可以访问 子元素可以访问 private 私有权限 类内可以访问 类外不可以访问 子元素不能访问 ``` 根据实际情况去进行设置即可 可以不写 不写默认是私有 #### struct和class区别 跟之前的结构体很相似 区别在于 默认的访问权限不同 ```cpp struct 默认权限为公共 class 默认权限为私有 class C1 { int m_A; //默认是私有权限 }; struct C2 { int m_A; //默认是公共权限 }; void main() { C1 c1; c1.m_A = 10; //错误,访问权限是私有 C2 c2; c2.m_A = 10; //正确,访问权限是公共 } ``` ![[cpp-image38.png]] #### 成员属性设置为私有(类似get访问器) 将所有成员属性设置为私有,可以自己控制读写权限(通过访问器修改数值 而非直接修改) 对于写权限,可以检测数据的有效性(比如设置年龄等有范围的数据时 可以先进行一次判断) ```cpp class Person { private: string m_Name; //可读可写 姓名 int m_Age; //只读 年龄 public: //姓名设置可读可写 void setName(string name) { m_Name = name; } string getName() { return m_Name; } //获取年龄 int getAge() { return m_Age; } //设置年龄 void setAge(int age) { if (age < 0 || age > 150) { cout << "请输入正确数值!" << endl; return; } m_Age = age; } }; int main() { Person p; //姓名设置 p.setName("张三"); cout << "姓名: " << p.getName() << endl; //年龄设置 p.setAge(50); cout << "年龄: " << p.getAge() << endl; return 0; } ``` #### 构造与析构 在前面 ![[cpp-image39.png]] 对于person这个类 实例出来p 需要在之后再调用方法进行初始化 不像int i=10;这般可以连在一起写 要实现这种 其实类都会有构造函数 如果自己不定义 那系统也会默认调用 那就是默认啥也没有(空实现) 要想声明时就做好初始化其实就是让程序调用自己的构造函数 给出数值作为初始化的数据 然后另一方面 类这些东西归根结底是自己创造的 需要自己释放 然后就有着析构函数 这对应着现实也很形象 比如生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全 面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置。 构造函数: 创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。 类名(){} 构造函数,没有返回值也不写void 函数名称与类名相同 构造函数可以有参数,因此可以发生重载 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次 析构函数: 对象销毁前系统自动调用,执行一些清理工作。 ~类名(){} 析构函数,没有返回值也不写void 函数名称与类名相同,在名称前加上符号 ~ 析构函数不可以有参数,因此不可以发生重载 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次 写在public作用域下 ```cpp public: Person() { cout<<"构造函数"< using namespace std; class Person{ public: static int count; // 静态成员变量,用于计数 Person(int Age) { age = Age; id = count++; // 使用计数器分配唯一的标识符 cout << "构造函数调用,对象标识:" << id << endl; } ~Person() { cout << "析构函数调用,对象标识:" << id << endl; } void Print() { cout << "对象标识:" << id << ",年龄为:" << age << endl; } private: int age; int id; }; int Person::count = 0; // 初始化静态成员变量 int main(){ Person a(10); Person b(20); a.Print(); // 输出对象标识和年龄 b.Print(); return 0; } ``` 添加了一个静态成员变量count,用于计数对象的数量。在构造函数中,我们将静态成员变量count的值赋给对象的标识符id,并将count自增。这样每个对象都有一个唯一的标识符。 在构造函数和析构函数中,我们输出对象的标识符,以区分不同的对象。 ##### 显式调用与创建位置 构造、析构函数是可以显式调用的 ```cpp Person* p = new Person(10); // 显式调用构造函数 delete p; // 显式调用析构函数并释放内存 ``` 这让我再次联想到c# 然后又对于对象实例位置进行了一些实验 在c#中 用Person p = new Person(10); // 创建对象并赋值给变量 构建一个实例 但是为什么 new返回的是指针 而csharp中可以直接使用一个自定义Person类型去接收 是因为 在C#中,对象的创建和内存管理由运行时环境(CLR)自动处理,在堆上分配内存来存储对象,并将对象的引用赋值给变量p。C#中的对象变量实际上存储的是对象的引用,而不是对象本身。通过引用,你可以访问对象的成员和方法。 这种语法使得对象的创建和内存管理更加方便,而不需要显式处理指针和手动的内存分配。在C#中,垃圾回收器会自动跟踪对象的引用和内存使用情况,并在对象不再被引用时自动释放内存。 而在c++中 Person a(10):使用构造函数初始化的方式,在栈上创建了一个名为 a 的 Person 对象,并将参数 10 传递给构造函数进行初始化。 Person b = Person(10):使用复制构造函数进行初始化的方式,在栈上创建了一个名为 b 的 Person 对象。这里的 Person(10) 创建了一个临时的 Person 对象,并将其拷贝给 b,触发了复制构造函数的调用。 Person c = 10:使用隐式转换进行初始化的方式,在栈上创建了一个名为 c 的 Person 对象。这里的 10 会隐式地转换为一个临时的 Person 对象,然后将其拷贝给 c,同样触发了复制构造函数的调用。 在这些情况下,对象的创建都是在栈上进行的,不涉及 new 运算符进行堆上的内存分配。这些对象的生命周期受限于其所在的作用域,当对象超出作用域时,会自动调用析构函数进行资源的清理和对象的销毁。 常用的这三种其实压根和new无关 它们都是作用在栈区 而不跟c#一样在堆区 C++中也可以让对象创建在堆区 那就是前面提到的显式调用 ![[cpp-image47.png]] 使用 new 运算符在堆上分配了一个 Person 对象实例,并将其赋值给指针 p。你需要在不再需要对象时手动调用 delete 运算符来释放内存 ##### 构造函数调用规则 默认情况下,c++编译器至少给一个类添加3个函数 1.默认构造函数(无参,函数体为空) 2.默认析构函数(无参,函数体为空) 3.默认拷贝构造函数,对属性进行值拷贝 构造函数调用规则如下: 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造 如果用户定义拷贝构造函数,c++不会再提供其他构造函数 自己定义1 系统仍提供23 自己定义2 系统仍提供3 不提供1 自己定义3 系统不提供12 但一般定义3就要定义2 所以自定义23缺失1 ##### 深浅拷贝问题 C++中的深拷贝(deep copy)和浅拷贝(shallow copy)是与对象拷贝相关的概念。 浅拷贝是指将一个对象的数据成员的值复制到另一个对象,包括指针的值也会被复制,但是指针所指向的内存地址不会被复制。这意味着如果原始对象和拷贝对象中的指针指向同一块内存地址,当一个对象修改这块内存时,另一个对象也会受到影响。 深拷贝是指在拷贝对象时,不仅复制数据成员的值,而且对于指针类型的成员,也会复制指针所指向的内存地址上的数据。这样,原始对象和拷贝对象就各自拥有独立的内存空间,彼此之间的修改互不影响。 在C++中,默认的拷贝构造函数和赋值运算符执行的是浅拷贝操作。如果类中包含了指针类型的成员变量,那么需要自定义拷贝构造函数和赋值运算符来实现深拷贝,以确保在对象拷贝时,指针指向的内存数据也能得到正确地复制。 (成员变量没指针类型就无所谓 一旦有指针变量这种浅拷贝就会带来一系列问题诸如值同步变化、重复释放 这就需要使用深拷贝 在堆区自己创建空间进行拷贝释放等操作) 浅拷贝: ```cpp #include class MyClass { private: int* data; public: MyClass(int value) { data = new int(value); } // 拷贝构造函数 MyClass(const MyClass& other) { data = other.data; // 浅拷贝,指针复制 } // 赋值运算符 MyClass& operator=(const MyClass& other) { if (this != &other) { delete data; // 释放原有内存 data = other.data; // 浅拷贝,指针复制 } return *this; } int getData() const { return *data; } void setData(int value) { *data = value; } ~MyClass() { delete data; } }; int main() { MyClass obj1(10); MyClass obj2 = obj1; // 调用拷贝构造函数,进行浅拷贝 obj2.setData(20); // 修改obj2的数据 std::cout << "obj1 data: " << obj1.getData() << std::endl; std::cout << "obj2 data: " << obj2.getData() << std::endl; return 0; } ``` 在上面的示例中,MyClass类包含一个指针类型的成员变量data,在拷贝构造函数和赋值运算符中,进行的是浅拷贝操作。当修改obj2的数据时,obj1的数据也会被修改,因为它们共享同一块内存。 为了实现深拷贝,我们需要对拷贝构造函数和赋值运算符进行改进: 深拷贝 ```cpp MyClass(const MyClass& other) { data = new int(*other.data); // 深拷贝,复制指针所指向的内存数据 } // 赋值运算符(深拷贝) MyClass& operator=(const MyClass& other) { if (this != &other) { delete data; // 释放原有内存 data = new int(*other.data); // 深拷贝,复制指针所指向的内存数据 } return *this; } ``` 通过在拷贝构造函数和赋值运算符中创建新的内存空间,并将指针所指向的数据复制过去,就实现了深拷贝。这样,即使修改其中一个对象的数据,另一个对象也不会受到影响。 同时 使用深拷贝时,需要在适当的时候手动释放动态分配的内存,以防止内存泄漏。通常在类的析构函数中释放这些内存是一个好的做法 ```cpp ~MyClass() { cout << "析构函数!" << endl; if (data != NULL) { delete data; } } ``` ##### 初始化列表 构造函数():属性1(值1),属性2(值2)... {} 就是简单把 ```cpp Person(int age, int height) { m_age = age; m_height = new int(height); } ``` 替换成 ```cpp Person(int age ,int height):m_age(age),m_height(new int(height)) { } ``` #### 类包含类(类对象作为类成员) ```cpp class point{} class circle{…} ``` 要创圆就得有点 所以调用时先会创建point 然后就都知道了 因为函数啊类啊什么的都是栈似的 point进 circle进 circle出 point出 ```cpp …… ``` #### 静态成员 静态成员是指属于类本身而不是类的实例的成员。它们在整个类的所有对象之间是共享的,而不是每个对象拥有自己的独立副本。静态成员可以包括静态变量和静态函数。 静态变量: 静态变量是在类中声明的变量,使用关键字 "static" 进行修饰。静态变量只会被创建一次,无论类的对象有多少个。它们在类的所有对象之间共享相同的存储空间。静态变量可以通过类名和作用域解析运算符(::)直接访问,而不需要创建类的对象。 静态函数: 静态函数是属于类而不是类的对象的成员函数。它们可以在没有创建类的对象的情况下被调用。静态函数只能访问类的静态成员变量和其他静态成员函数,不能访问非静态(普通)成员变量和成员函数,因为非静态成员需要通过对象才能访问。 静态成员的特点和用途: 共享性:静态成员在所有类的对象之间共享,对于需要在多个对象之间共享数据的情况非常有用。 生命周期:静态成员的生命周期从程序开始到程序结束,与类的对象的生命周期无关。 访问权限:静态成员可以在类的内部和外部进行访问,甚至在没有创建类的对象的情况下也可以直接访问。 静态成员可以用于以下情况: 统计和共享数据:通过静态变量可以在多个对象之间共享数据,例如统计类的实例数量。 公用函数:通过静态函数可以定义不依赖于具体对象的通用功能。 常量定义:静态成员变量可以用于定义类级别的常量。 ```cpp #include class MyClass { private: static int staticVariable; // 静态成员变量 public: static void staticFunction() { // 静态成员函数 std::cout << "Static Function" << std::endl; } void nonStaticFunction() { // 非静态成员函数 std::cout << "Non-Static Function" << std::endl; std::cout << "Static Variable: " << staticVariable << std::endl; } }; int MyClass::staticVariable = 0; // 静态成员变量的定义和初始化 int main() { MyClass::staticFunction(); // 通过类名调用静态函数 MyClass obj1; obj1.nonStaticFunction(); // 通过对象调用非静态函数 MyClass obj2; obj2.nonStaticFunction(); obj1.nonStaticFunction(); // 类的静态变量在所有对象之间共享 return 0; } ``` MyClass类包含一个静态成员变量staticVariable和两个成员函数staticFunction()和nonStaticFunction()。静态函数可以直接通过类名调用,而非静态函数需要通过类的对象调用。静态变量staticVariable在所有对象之间共享,可以通过类名或对象访问。 静态成员对于在类级别共享数据和定义通用功能非常有用。它们可以提供一种在类的所有对象之间共享信息的方式,并且不需要每个对象都拥有自己的副本。 ```cpp #include using namespace std; class Person{ public: static int count; // 静态成员变量,用于计数 Person(){ } Person(int Age) { age = Age; id = count++; cout << "构造函数调用,对象标识:" << getLetterId(id) << endl; } Person(const Person &Age) { age = Age.age; id = count++; cout << "构造函数调用,对象标识:" << getLetterId(id) << endl; } ~Person() { cout << "析构函数调用,对象标识:" << getLetterId(id) << endl; } void Print(const std::string& caller) { cout<age = age; } Person& PersonAddPerson(Person p) {//要返回引用 如果返回值会拷贝成一个新实例来返回 而不是原实例 this->age += p.age; //返回对象本身 return *this; } int age; }; void test01(){ Person p1(10); cout << "p1.age = " << p1.age << endl; Person p2(10); p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1); cout << "p2.age = " << p2.age << endl; } void main() { test01(); return 0; } ``` ##### const与this指针 ```cpp //this指针的本质是一个指针常量,指针的指向不可修改 //如果想让指针指向的值也不可以修改,需要声明常函数 void ShowPerson() { this->mA = 100; //this指针指向的对象的数据可以修改 this = NULL; //不能修改指针的指向 Person* const this; } ``` 要想this->mA的值也不能被修改 需要再加个const 但对于这么一个函数/方法 已经没什么位置可写了 只能写在void ShowPerson() const {} 函数名后面 这样就相当于是const Person * const this; 如此一来 一般的成员变量就无法被修改 但是加了mutable关键字的例外 ```cpp class Person { public: void ShowPerson() const { this->mA = 100; //const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量 this->m_B = 100; } private: int m_A; mutable int m_B; //可修改 可变的 }; ``` 常对象: ```cpp const Person person; //常量对象 cout << person.m_A << endl; //person.mA = 100; //常对象不能修改成员变量的值,但是可以访问 person.m_B = 100; //但是常对象可以修改mutable修饰成员变量 ``` #### 友元 友元(friend)是C++中的一个特性,它允许一个类或函数访问另一个类的私有成员。友元关系通常在类定义中进行声明,在类的声明中使用friend关键字来指定其他类或函数为友元。 友元关系的目的是允许一些特定的外部实体(类或函数)访问另一个类的私有成员,从而在一定程度上突破了封装性。然而,友元应该谨慎使用,因为它打破了类的封装性和隐藏实现的原则,增加了代码的耦合性。只有在确实有必要且安全的情况下才应该使用友元。 友元可以分为两种类型: 友元类:一个类可以将其他类声明为友元类,使其具有访问该类的私有成员的权限。 友元函数:一个函数可以被声明为某个类的友元函数,使其能够访问该类的私有成员。 ```cpp #include class MyClass { private: int privateData; public: MyClass() : privateData(0) {}//初始化列表赋初值 friend class FriendClass; // 声明FriendClass为MyClass的友元类 friend void friendFunction(const MyClass& obj); // 声明friendFunction为MyClass的友元函数 }; class FriendClass { public: void accessPrivateData(const MyClass& obj) { std::cout << "FriendClass accessing privateData: " << obj.privateData << std::endl; } }; void friendFunction(const MyClass& obj) { std::cout << "friendFunction accessing privateData: " << obj.privateData << std::endl; } int main() { MyClass obj; FriendClass friendObj; friendObj.accessPrivateData(obj); // FriendClass访问私有成员 friendFunction(obj); // friendFunction访问私有成员 return 0; } ``` FriendClass被声明为MyClass的友元类,这样FriendClass就能够访问MyClass的私有成员privateData。同样,friendFunction被声明为MyClass的友元函数,也可以访问MyClass的私有成员。 需要注意的是,友元关系是单向的,即如果类A将类B声明为友元类,则类B不会自动将类A声明为友元类。友元关系也不具有传递性,即如果类A是类B的友元,而类B又是类C的友元,那么类C并不会自动成为类A的友元。 (即不会自动设 一切都需要手动设置) 再次强调,友元关系应该谨慎使用,因为它打破了类的封装性。只有在确实有必要让某个类或函数访问私有成员,并且能够确保安全性的情况下,才应该使用友元。 还有一种 类2中函数1为类1的友元函数 仅它有访问权限 综合案例: ```cpp #include #include using namespace std; class Building; class friendclass{ public: friendclass(); void visit(); private: Building *building; }; class friendbodyfunction{ public: friendbodyfunction(); void visit(); void visit2(); private: Building *building; }; class Building{ friend void friendfunction(Building *building); friend class friendclass; friend void friendbodyfunction::visit(); public: Building() { sittingroom="客厅"; bedroom="卧室"; } public: string sittingroom; private: string bedroom; }; void friendfunction(Building *building){ cout<sittingroom<bedroom<sittingroom<bedroom<sittingroom << endl; cout << building->bedroom << endl; } void friendbodyfunction::visit2() { cout << building->sittingroom << endl; // cout << building->bedroom << endl;} int main(){ Building a; friendfunction(&a); friendclass b; b.visit(); friendbodyfunction c; c.visit(); return 0; } ``` 顺序不能错 前向声明Building类: ```cpp class Building; ``` 在代码的开头,使用了前向声明来声明Building类,这样可以在后续的代码中使用Building*指针类型,而不需要提前定义Building类的完整细节。这是为了解决后续的friendclass类和friendbodyfunction类与Building类之间的相互引用问题。如果没有前向声明,那么在friendclass类和friendbodyfunction类中使用指向Building的指针成员是无法实现的。 使用friendclass类: ```cpp class friendclass{ public: friendclass(); void visit(); private: Building *building; // 使用指向Building的指针成员 }; ``` 在friendclass类的定义中,使用了指向Building的指针成员Building *building;,因为friendclass类是Building类的友元类。通过前向声明,编译器知道Building类的存在,从而可以在friendclass类中使用指向Building的指针。 使用friendbodyfunction类: ```cpp class friendbodyfunction { public: friendbodyfunction(); void visit(); void visit2(); private: Building *building; // 使用指向Building的指针成员 }; ``` 在friendbodyfunction类的定义中,也使用了指向Building的指针成员Building *building;,因为friendbodyfunction类是Building类的友元类。通过前向声明,编译器知道Building类的存在,从而可以在friendbodyfunction类中使用指向Building的指针。 使用friendfunction函数: ```cpp void friendfunction(Building *building) { cout<sittingroom<bedroom<m_A + p.m_A; temp.m_B = this->m_B + p.m_B; return temp; } } ``` 调用:Person p3 = p2 + p1; //相当于 p2.operator+(p1) ```cpp //全局函数实现 + 号运算符重载 Person operator+(const Person& p1, const Person& p2) { Person temp(0, 0); temp.m_A = p1.m_A + p2.m_A; temp.m_B = p1.m_B + p2.m_B; return temp; } ``` 调用:Person p3 = p2 + p1//相当于 operator+(p2,p1) (+-*/直接换就行) ##### 左移(输出)重载 希望实现自定义的东西能直接cout流输出 而不是p.什么什么一个个输出 左移运算符重载成员函数 实现不了 `p2.operator<<(cout)` 或者 `p2.operator<<(p1)`,简化后是 `p << cout` 亦或是 `p2<m_Name = name; this->m_Age = age; }; bool operator==(Person3 & p) { if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) { return true; } else { return false; } } bool operator!=(Person3 & p) { if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) { return false; } else { return true; } } string m_Name; int m_Age; }; void test06(){ Person3 a("张三", 18); Person3 b("张三", 18); if (a == b) { cout << "a和b相等" << endl; } else { cout << "a和b不相等" << endl; } if (a != b) { cout << "a和b不相等" << endl; } else { cout << "a和b相等" << endl; }} ``` ##### ()重载 在C++中,可以通过运算符重载来定义自定义的函数对象,也称为仿函数(Functor)。仿函数是重载了函数调用运算符 `operator()` 的类,它可以像函数一样被调用,因此具有类似函数的行为。通过仿函数,我们可以在代码中像使用函数一样使用对象,这在某些情况下可以使代码更加简洁和灵活。 仿函数来计算两个数的乘积: ```cpp #include class Multiplier { public: int operator()(int x, int y) const { return x * y; } }; int main() { Multiplier multiply; // 创建一个仿函数对象 int result = multiply(5, 3); // 使用仿函数对象进行函数调用,计算5和3的乘积 std::cout << "Result: " << result << std::endl; return 0; } ``` 定义了一个名为`Multiplier`的类,并在其中重载了函数调用运算符 `operator()`. 仿函数的调用实际上就是对 `operator()` 的调用。在 `main()` 函数中,我们创建了一个 `Multiplier` 类的对象 `multiply`,然后通过 `multiply(5, 3)` 调用它。这样我们就得到了 5 和 3 的乘积,并输出结果为 `Result: 15`。 通过仿函数,我们可以在类中保持状态,可以将函数对象看作具有状态的函数。这样,我们可以在函数调用之间保持一些信息,而函数的行为也可以基于这些信息进行变化。 仿函数常常在标准库中使用,特别是在使用STL的时候,如STL算法中的排序函数`std::sort()`,我们可以通过自定义的仿函数来指定排序规则。 使用自定义仿函数 ```cpp #include #include #include class CustomComparator { public: bool operator()(int a, int b) const { return a > b; // 降序排序 } }; int main() { std::vector numbers = {5, 2, 8, 1, 3}; std::sort(numbers.begin(), numbers.end(), CustomComparator()); for (int num : numbers) { std::cout << num << " "; } return 0; } ``` 定义了一个名为`CustomComparator`的仿函数类,重载了函数调用运算符`operator()`,以实现降序排序。然后,我们使用自定义的仿函数对象 `CustomComparator()` 作为 `std::sort()` 函数的第三个参数,从而指定排序规则为降序。输出结果为 `8 5 3 2 1`,按照降序排列了原始向量中的元素。 通过仿函数,我们可以以非常灵活的方式定制各种功能,使得代码更加清晰、可读和可维护。 ##### 重载系列代码: ```cpp #include using namespace std; class Person{ friend Person operator+(const Person& p1, const Person& p2); friend ostream& operator<<(ostream &cout,Person &p); public: Person() { } Person(int a, int b):m_A(a),m_B(b){ } void setm_A(int a){ m_A=a; } int getm_A(){ return m_A; } void setm_B(int b){ m_B=b; } int getm_B(){ return m_B; } //成员函数实现+-*/运算符重载 Person operator*(Person &p) { Person temp; temp.m_A=this->m_A*p.m_A; temp.m_B=this->m_B*p.m_B; return temp; } //调用相当于 p2.operator+(p1) 简写 p2+p1 // 左移运算符重载 // 成员函数 实现不了 // p2.operator<<(cout)或者 p2.operator<<(p1) // 简化后是p << cout 亦或是 p2<m_Name = name; this->m_Age = age; }; bool operator==(Person3 & p) { if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) { return true; } else { return false; } } bool operator!=(Person3 & p) { if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) { return false; } else { return true; } } string m_Name; int m_Age; }; void test06(){ Person3 a("张三", 18); Person3 b("张三", 18); if (a == b) { cout << "a和b相等" << endl; } else { cout << "a和b不相等" << endl; } if (a != b) { cout << "a和b不相等" << endl; } else { cout << "a和b相等" << endl; } } class Multiplier { public: int operator()(int x, int y) const { return x * y; } }; void test07(){ Multiplier multiply; // 创建一个仿函数对象 int result = multiply(5, 3); // 使用仿函数对象进行函数调用,计算5和3的乘积 cout << "Result: " << result << endl; } int main() { // test01(); // test02(); // test03(); // test04(); // test05(); // test06(); test07(); return 0; } ``` ### 继承 C++中的继承是面向对象编程的一个重要概念,允许一个类(称为子类或派生类)从另一个类(称为父类或基类)继承属性和行为。子类可以使用父类中已经定义的成员变量和成员函数,并且可以添加自己的新成员变量和成员函数。 在C++中,继承通过在类的定义中使用冒号(:)后跟父类的名称来实现。语法如下: ```cpp class BaseClass { // Base class members and functions }; class DerivedClass : public BaseClass { // Derived class members and functions }; ``` DerivedClass 是从 BaseClass 继承而来的子类。通过关键字 public(还可以是private或protected)来声明继承方式。public 继承表示子类可以访问父类的公共成员和受保护成员。 继承的特点包括: 访问控制: 公共继承:基类中的公共成员在派生类中保持公共访问权限。 保护继承:基类中的公共成员在派生类中变为受保护访问权限。 私有继承:基类中的公共成员在派生类中变为私有访问权限。 构造函数和析构函数: 派生类的构造函数和析构函数会自动调用基类的构造函数和析构函数,确保基类和派生类的对象都能正确初始化和销毁。 派生类的功能增强: 派生类可以通过添加新的成员函数和成员变量来扩展基类的功能,或者重写(覆盖)基类的成员函数以实现特定的行为。 多重继承: C++允许一个类从多个基类继承,这就是所谓的多重继承。 虚函数和多态: 使用虚函数可以在派生类中实现运行时多态,允许在基类指针或引用下调用派生类的特定实现。 #### 继承中的对象模型 从父类继承过来的成员,哪些属于子类对象中? ```cpp class Base { public: int m_A; protected: int m_B; private: int m_C; //私有成员只是被隐藏了,但是还是会继承下去 }; //公共继承 class Son :public Base { public: int m_D; }; void test01() { cout << "sizeof Son = " << sizeof(Son) << endl; //16 } ``` 父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到 在子类中占空间 这里就是继承了12字节 自己加4字节 一共16字节 #### 继承同名成员处理方式 当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢? 访问子类同名成员 直接访问即可 访问父类同名成员 需要加作用域 ```cpp class Base { public: Base() { m_A = 100; } void func() { cout << "Base - func()调用" << endl; } void func(int a) { cout << "Base - func(int a)调用" << endl; } public: int m_A; }; class Son : public Base { public: Son() { m_A = 200; } //当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数 //如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域 void func() { cout << "Son - func()调用" << endl; } public: int m_A; }; void test01() { Son s; cout << "Son下的m_A = " << s.m_A << endl; cout << "Base下的m_A = " << s.Base::m_A << endl; s.func(); s.Base::func(); s.Base::func(10); } ``` 子类对象可以直接访问到子类中同名成员 子类对象加作用域可以访问到父类同名成员 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数 #### 继承同名静态成员处理方式 继承中同名的静态成员在子类对象上如何进行访问? 静态成员和非静态成员出现同名,处理方式一致 访问子类同名成员 直接访问即可 访问父类同名成员 需要加作用域 ```cpp class Base { public: static void func() { cout << "Base - static void func()" << endl; } static void func(int a) { cout << "Base - static void func(int a)" << endl; } static int m_A; }; int Base::m_A = 100; class Son : public Base { public: static void func() { cout << "Son - static void func()" << endl; } static int m_A; }; int Son::m_A = 200; //同名成员属性 void test01() { //通过对象访问 cout << "通过对象访问: " << endl; Son s; cout << "Son 下 m_A = " << s.m_A << endl; cout << "Base 下 m_A = " << s.Base::m_A << endl; //通过类名访问 cout << "通过类名访问: " << endl; cout << "Son 下 m_A = " << Son::m_A << endl; cout << "Base 下 m_A = " << Son::Base::m_A << endl; } //同名成员函数 void test02() { //通过对象访问 cout << "通过对象访问: " << endl; Son s; s.func(); s.Base::func(); cout << "通过类名访问: " << endl; Son::func(); Son::Base::func(); //出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问 Son::Base::func(100); } int main() { //test01(); test02(); return 0; } ``` 同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名) #### 多继承 允许一个类继承多个类 ```cpp class 子类 :继承方式 父类1 , 继承方式 父类2... ``` 多继承可能会引发父类中有同名成员出现,需要加作用域区分 实际开发中不建议用多继承 ```cpp #include using namespace std; class Base1 { public: Base1() { m_A = 100; } public: int m_A; }; class Base2 { public: Base2() { m_A = 200; //开始是m_B 不会出问题,但是改为mA就会出现不明确 } public: int m_A; }; //语法:class 子类:继承方式 父类1 ,继承方式 父类2 class Son : public Base2, public Base1{ public: Son() { m_C = 300; m_D = 400; } public: int m_C; int m_D; }; //多继承容易产生成员同名的情况 //通过使用类名作用域可以区分调用哪一个基类的成员 void test01(){ Son s; cout << "sizeof Son = " << sizeof(s) << endl; cout << s.Base1::m_A << endl; cout << s.Base2::m_A << endl; } int main() { test01(); return 0; } ``` #### 菱形继承 概念: 两个派生类继承同一个基类 又有某个类同时继承者两个派生类 这种继承被称为菱形继承,或者钻石继承 ![[cpp-image49.png]] 羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时,就会产生二义性。 羊驼继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。 解决菱形继承问题的常用方法是使用虚继承。虚继承可以确保共同基类 在派生类中只有一个实例,从而解决了多次继承同一基类的问题。通过在 俩子类 的继承声明中使用 virtual 关键字来实现虚继承 ```cpp class Sheep : virtual public Animal { }; class Tuo : virtual public Animal { }; class SheepTuo : public Sheep, public Tuo { }; ``` 使用虚继承后,羊驼类中只会有一个来自animal的成员变量,从而解决了菱形继承带来的二义性问题。 虚继承解决这个问题的原理是引入一个虚基类指针(virtual base pointer,vbptr)和一个虚基类表(virtual base table,vtable)来跟踪虚基类的位置。在菱形继承中,如果某个类是虚继承的,则其虚基类子对象在内存布局中只会有一个实例,而不会像普通继承一样在派生类中存在多个实例。 当某个类使用虚继承时,编译器会在该类的内存布局中添加一个隐藏的指针,即虚基类指针 vbptr。而这个指针会指向一个虚基类表,其中存储着偏移量信息, 用于在运行时找到虚基类的位置。 ```cpp class Rectangle : public virtual Shape { // ... }; class Triangle : public virtual Shape { // ... }; ``` Rectangle 和 Triangle 都将 Shape 声明为虚基类 ```cpp class Diamond : public Rectangle, public Triangle { // ... }; ``` Diamond 类通过虚继承来继承 Rectangle 和 Triangle 由于 Rectangle 和 Triangle 都虚继承自 Shape,Diamond 类中只会有一个 Shape 子对象。虚继承使得 Diamond 类的内存布局变为 +----------------------+ | Rectangle vbptr | ---+ +----------------------+ \ | Triangle vbptr | \ +----------------------+ \ | Shape | \ Shared Shape sub-object +----------------------+ \ in Diamond | Rectangle data | / +----------------------+ / | Triangle data | / +----------------------+ / | Diamond data | ---+ +----------------------+ 通过虚继承,Diamond 类中只有一个 Shape 子对象,虚基类指针 vbptr 指向了 Shape 在内存中的位置。这样就避免了菱形继承带来的二义性问题,确保了 Diamond 类中只有一个 Shape 成员,从而解决了多次继承同一基类的问题。 ### 多态 多态分为两类 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名 动态多态: 派生类和虚函数实现运行时多态 静态多态和动态多态区别: 静态多态的函数地址早绑定 - 编译阶段确定函数地址 动态多态的函数地址晚绑定 - 运行阶段确定函数地址 #### 使用virtual实现 ```cpp class Animal { public: void speak() { cout << "动物在说话" << endl; } }; class Cat :public Animal { public: void speak() { cout << "小猫在说话" << endl; } }; class Dog :public Animal { public: void speak() { cout << "小狗在说话" << endl; } }; void DoSpeak(Animal & animal) { animal.speak(); } int main() { Cat cat; DoSpeak(cat);//结果是输出 动物在说话 而非cat Dog dog; DoSpeak(dog);//结果是输出 动物在说话 而非dog return 0; } ``` 因为此时 speak函数是已确定的 无论传入什么进去 都只能调用父类中的 如果想改成 传入什么 就调用什么的speak函数 就需要加上virtual关键字 使其变为虚函数 从而使得animal.speak()不一开始就确定 而是根据传入对象而选择调用哪个 ```cpp class Animal { public: //Speak函数就是虚函数 //函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。 virtual void speak() { cout << "动物在说话" << endl; } }; class Cat :public Animal { public: void speak() { cout << "小猫在说话" << endl; } }; class Dog :public Animal { public: void speak() { cout << "小狗在说话" << endl; } }; //我们希望传入什么对象,那么就调用什么对象的函数 //如果函数地址在编译阶段就能确定,那么静态联编 //如果函数地址在运行阶段才能确定,就是动态联编 void DoSpeak(Animal & animal) { animal.speak(); } //多态满足条件: //1、有继承关系 //2、子类重写父类中的虚函数 //多态使用: //父类指针或引用指向子类对象 void test01() { Cat cat; DoSpeak(cat); Dog dog; DoSpeak(dog); } int main() { test01(); system("pause"); return 0; } ``` 我们定义了一个基类Animal和两个派生类Cat和Dog。Animal类中声明了一个虚函数speak(),并在派生类Cat和Dog中重写(override)了这个虚函数。 虚函数的存在使得我们可以通过基类指针或引用来实现多态性。在main函数中,我们定义了Cat对象和Dog对象,并通过DoSpeak函数来调用它们的speak()函数。 函数DoSpeak(Animal & animal)是一个非常重要的函数,它接受一个Animal类的引用作为参数。由于speak()函数是虚函数,它将在运行时根据实际对象的类 型来确定调用的版本,而不是在编译时根据指针或引用的类型来确定。 创建Cat对象cat并调用DoSpeak(cat): a. DoSpeak(cat)函数接受一个Animal引用,但在调用时传入了Cat对象cat。由于DoSpeak函数的参数是Animal引用,编译器会对传入的cat对象进行向上转型(upcast),将它视为Animal对象。这样做是因为Cat是Animal的派生类。 b. 然后,DoSpeak函数调用animal.speak()。由于speak()是虚函数,并且animal指向的实际对象是Cat,所以运行时会调用Cat类的speak()函数。因此,输出是"小猫在说话"。 创建Dog对象dog并调用DoSpeak(dog): a. 同样地,DoSpeak(dog)函数接受一个Animal引用,但在调用时传入了Dog对象dog。由于DoSpeak函数的参数是Animal引用,编译器会对传入的dog对象进行向上转型,将它视为Animal对象。 b. 然后,DoSpeak函数调用animal.speak()。由于speak()是虚函数,并且animal指向的实际对象是Dog,所以运行时会调用Dog类的speak()函数。因此,输出是"小狗在说话"。 通过虚函数的多态性,我们可以在运行时根据对象的实际类型来调用适当的函数,而不是在编译时确定。这样可以方便地处理不同类型的派生类对象,并使代码更加灵活和可扩展。 #### 虚函数原理 还是以上的例子: Animal类中的speak()函数被声明为虚函数。 Cat和Dog类分别继承自Animal,并在派生类中重写了speak()函数。 当我们创建Cat对象cat和Dog对象dog并调用它们的speak()函数时, 发生了以下过程: 对于cat.speak(): 编译器会在编译时将speak()标记为虚函数,并在Cat类的虚函数表中填入`Cat::speak()`的地址。 cat对象包含一个隐藏的虚函数指针(vptr),指向Cat类的虚函数表。 运行时,通过cat对象的虚函数指针找到Cat类的虚函数表,并调用其中存储的speak()函数地址,输出 "小猫在说话"。 对于dog.speak(): 同样的,编译器会在编译时将speak()标记为虚函数,并在Dog类的虚函数表中填入`Dog::speak()`的地址。 dog对象包含一个隐藏的虚函数指针(vptr),指向Dog类的虚函数表。 运行时,通过dog对象的虚函数指针找到Dog类的虚函数表,并调用其中存储的speak()函数地址,输出 "小狗在说话"。 虚函数表: 基类Animal的虚函数表: 假设Animal类有两个虚函数:virtual void speak() 和 virtual void eat() Animal类会在编译阶段生成自己的虚函数表,并将这两个虚函数的地址填入虚函数表中。 派生类Cat继承Animal: 当Cat类继承Animal类时,它会拥有一个自己的虚函数表,但并不会复制基类Animal的虚函数表。 Cat类会在自己的虚函数表中重新填充虚函数的地址,只有在Cat类中重新实现的虚函数才会填入新的地址,其他的虚函数指针将继续指向基类Animal的虚函数表中对应的函数地址。 派生类Dog继承Animal: 类似地,派生类Dog也会有自己的虚函数表,只有在Dog类中重新实现的虚函数会填入新的地址,没有重新实现的虚函数指针将保持指向基类Animal的虚函数表中的函数地址。 继承并不是简单地将基类的虚函数表复制给派生类,而是让派生类在自己的虚函数表中重新填充或覆盖虚函数的地址,以实现自己的虚函数实现。如果派生类没有重新实现某个虚函数,它的虚函数表中相应的指针将保持指向基类虚函数表中对应的函数地址,从而实现继承的多态性。这样,通过派生类对象调用虚函数时,会根据对象的实际类型来调用正确的虚函数实现。 ![[cpp-image50.png]] 未重写时: ![[cpp-image51.png]] 重写后: ![[cpp-image52.png]] 虚函数表(VTable)和虚函数指针(VPtr): 每个含有虚函数的类都有一个对应的虚函数表。虚函数表是一个指针数组,存储了类中声明为虚函数的成员函数的地址。 虚函数表在编译阶段生成,并存储在每个类的静态数据中。 类的实例对象(对象)在内存中通常包含一个隐藏的虚函数指针(vptr),指向其所属类的虚函数表。 虚函数指针在对象构造时由构造函数自动初始化。 继承和虚函数表: 派生类继承基类时,它拥有一个自己的虚函数表。并不是直接复制基类的虚函数表给派生类。 派生类的虚函数表中重新填充或重写派生类中重新实现的虚函数,其他虚函数指针保持指向基类虚函数表中对应虚函数的地址。 多态性: 继承和虚函数表的机制实现了多态性。当通过基类指针或引用调用虚函数时,运行时会根据对象的实际类型来调用正确的虚函数实现。 在案例中,DoSpeak()函数接受一个Animal引用,但可以传入Cat对象或Dog对象,它们实际上是Animal的派生类。 在运行时,通过传入的对象的虚函数指针找到对应类的虚函数表,并根据虚函数表中的地址调用正确的虚函数实现。 #### 纯虚函数、抽象类 纯虚函数(Pure Virtual Function): 纯虚函数是在基类中声明的虚函数,但没有给出具体的实现。它的声明以 "= 0" 结尾,告诉编译器该函数没有实际的定义。纯虚函数的目的是为了定义接口,即约定子类必须实现这个函数。 ```cpp class Shape { public: virtual void draw() = 0; // 纯虚函数 }; // Shape 是抽象类,因为它包含了一个纯虚函数,无法被实例化 // 派生类必须实现 draw() 函数 ``` 抽象类(Abstract Class): 抽象类是包含至少一个纯虚函数的类,它本身无法被实例化。抽象类用于定义接口,即规范子类应该具有的方法。子类必须实现抽象类中的纯虚函数,否则它们也将成为抽象类。 ```cpp class Shape { public: virtual void draw() = 0; // 纯虚函数 }; class Circle : public Shape { public: void draw() override { // 实现绘制圆形的代码 } }; class Square : public Shape { public: void draw() override { // 实现绘制正方形的代码 } }; // 错误示例:无法实例化抽象类 // Shape shape; // 错误,无法实例化抽象类 Shape* shape1 = new Circle(); // 正确,可以使用指针或引用指向派生类的对象 Shape* shape2 = new Square(); // 正确 ``` 纯虚函数是用 "= 0" 声明的虚函数,没有实际实现。 抽象类是包含至少一个纯虚函数的类,无法被实例化。 抽象类用于定义接口,强制子类实现特定的方法,实现多态性和继承的特性。 #### 虚析构和纯虚析构 多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码 (因为是用父指针构建的子对象 调用过程为:父构造 子构造 父析构 其中缺失了子析构 如果存在堆区数据的话 就无法被释放 导致内存泄漏) 解决方式:将父类中的析构函数改为虚析构或者纯虚析构 还是差不多原理 给一个指针和一个表 使系统能正确调用对应的函数 虚析构函数(Virtual Destructor): 虚析构函数是在基类中声明为虚函数的析构函数。当在使用继承的情况下,如果基类指针指向派生类对象,并且通过该指针删除对象时,如果基类的析构函数不是虚的,将导致不完全释放派生类资源,可能会产生内存泄漏或未定义行为。为了解决这个问题,通常将基类的析构函数声明为虚函数,从而确保在通过基类指针删除派生类对象时,会正确调用派生类的析构函数。 ```cpp class Base { public: virtual ~Base() { // 虚析构函数 // 在删除派生类对象时,将首先调用派生类的析构函数,然后再调用基类的析构函数 } }; class Derived : public Base { public: ~Derived() { // 派生类析构函数 } }; int main() { Base* ptr = new Derived(); // 在通过基类指针删除派生类对象时,会先调用 Derived 的析构函数,再调用 Base 的析构函数 delete ptr; return 0; } ``` 纯虚析构函数(Pure Virtual Destructor): 纯虚析构函数是一个在基类中声明为纯虚函数的析构函数。纯虚析构函数在基类中没有具体的实现,而是通过 "= 0" 来指示编译器。它的存在使得该类成为抽象类,并且该类不能被实例化。通常情况下,纯虚析构函数用于定义一个接口,要求派生类必须实现自己的析构函数。 ```cpp class Base { public: virtual ~Base() = 0; // 纯虚析构函数 }; Base::~Base() { // 纯虚析构函数需要在类的外部提供定义 // 但通常这里留空,因为它不需要实际实现 } class Derived : public Base { public: ~Derived() override { // 派生类析构函数 } }; int main() { // 错误示例:无法实例化抽象类 // Base obj; Base* ptr = new Derived(); // 在通过基类指针删除派生类对象时,会先调用 Derived 的析构函数,再调用 Base 的析构函数 delete ptr; return 0; } ``` 由于纯虚析构函数没有实际实现,所以基类中无法调用它。在使用纯虚析构函数的类继承体系中,派生类必须提供自己的析构函数实现。 虚析构和纯虚析构共性: 可以解决父类指针释放子类对象 都需要有具体的函数实现 虚析构和纯虚析构区别: 如果是纯虚析构,该类属于抽象类,无法实例化对象 虚析构语法: ```cpp virtual ~类名(){} ``` 纯虚析构语法: ```cpp virtual ~类名() = 0; ``` `类名::~类名(){}` ### 文件操作 程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放 通过文件可以将数据持久化 C++中对文件操作需要包含头文件 ==< fstream >== 文件类型分为两种: 文本文件 - 文件以文本的ASCII码形式存储在计算机中 二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们 操作文件的三大类: ofstream:写操作 ifstream: 读操作 fstream : 读写操作 #### 文本文件 写文件: 包含头文件#include 创建流对象ofstream ofs; 打开文件ofs.open("文件路径",打开方式); 写数据ofs << "写入的数据"; 关闭文件ofs.close(); | 打开方式 | 解释 | |---|---| | `ios::in` | 为读文件而打开文件 | | `ios::out` | 为写文件而打开文件 | | `ios::ate` | 初始位置:文件尾 | | `ios::app` | 追加方式写文件 | | `ios::trunc` | 如果文件存在先删除,再创建 | | `ios::binary` | 二进制方式 | 文件打开方式可以配合使用,利用|操作符 比如:用二进制方式写文件 `ios::binary` | `ios:: out` ```cpp #include using namespace std; #include void main(){ ofstream ofs; ofs.open("test.txt",ios::out); //out写文件会把之前内容清空重新写 要续写得app //ofs<<"姓名:张三"< 创建流对象ifstream ifs; 打开文件并判断文件是否打开成功ifs.open("文件路径",打开方式); 读数据四种方式读取 关闭文件ifs.close(); ```cpp #include using namespace std; #include #include void main() { ifstream ifs; ifs.open("test.txt",ios::in); if (!ifs.is_open()) { cout<<"文件打开失败"<>buf) // { // cout< using namespace std; #include class Person{ public: char m_name[64]; int m_age; }; void test03(){ // ofstream ofs; // ofs.open("person.txt",ios::out | ios::binary); ofstream ofs("person.txt",ios::out | ios::binary); Person p={"张三",18 }; ofs.write((const char *)&p,sizeof(Person)); ofs.close(); } ``` 文件输出流对象 可以通过write函数,以二进制方式写数据 读文件 二进制方式读文件主要利用流对象调用成员函数read 函数原型:istream& read(char *buffer,int len); 参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数 ```cpp #include using namespace std; #include void test04(){ ifstream ifs; ifs.open("person.txt",ios::in | ios::binary); if (!ifs.is_open()){ cout<<"失败"< using namespace std; #include #include void test01(){ ofstream ofs; ofs.open("test.txt",ios::out); ofs<<"姓名:张三"<>buf) // { // cout< using namespace std; class WorkerManager{ public: //构造函数 WorkerManager(); //析构函数 ~WorkerManager();}; ``` 3.3 源文件实现 在workerManager.cpp中将构造和析构函数空实现补全 ```cpp #include "workerManager.h"WorkerManager::WorkerManager(){}WorkerManager::~WorkerManager(){} ``` 至此职工管理类以创建完毕 4、菜单功能 功能描述:与用户的沟通界面 4.1 添加成员函数 在管理类workerManager.h中添加成员函数 void Show_Menu(); 4.2 菜单功能实现 在管理类workerManager.cpp中实现 Show_Menu()函数 ```cpp void WorkerManager::Show_Menu(){ cout << "********************************************" << endl; cout << "********* 欢迎使用职工管理系统! **********" << endl; cout << "************* 0.退出管理程序 *************" << endl; cout << "************* 1.增加职工信息 *************" << endl; cout << "************* 2.显示职工信息 *************" << endl; cout << "************* 3.删除离职职工 *************" << endl; cout << "************* 4.修改职工信息 *************" << endl; cout << "************* 5.查找职工信息 *************" << endl; cout << "************* 6.按照编号排序 *************" << endl; cout << "************* 7.清空所有文档 *************" << endl; cout << "********************************************" << endl; cout << endl; } ``` 4.3 测试菜单功能 在职工管理系统.cpp中测试菜单功能 代码: ```cpp #include using namespace std; #include "workerManager.h"int main() { WorkerManager wm; wm.Show_Menu(); system("pause"); return 0; } ``` 运行效果如图: 5、退出功能 5.1 提供功能接口 在main函数中提供分支选择,提供每个功能接口 代码: ```cpp int main() { WorkerManager wm; int choice = 0; while (true) { //展示菜单 wm.Show_Menu(); cout << "请输入您的选择:" << endl; cin >> choice; switch (choice) { case 0: //退出系统 break; case 1: //添加职工 break; case 2: //显示职工 break; case 3: //删除职工 break; case 4: //修改职工 break; case 5: //查找职工 break; case 6: //排序职工 break; case 7: //清空文件 break; default: system("cls"); break; } } system("pause"); return 0; } ``` 5.2 实现退出功能 在workerManager.h中提供退出系统的成员函数 void exitSystem(); 在workerManager.cpp中提供具体的功能实现 ```cpp void WorkerManager::exitSystem() { cout << "欢迎下次使用" << endl; system("pause"); exit(0); } ``` 5.3测试功能 在main函数分支 0 选项中,调用退出程序的接口 运行测试效果如图: 6、创建职工类 6.1 创建职工抽象类 职工的分类为:普通员工、经理、老板 将三种职工抽象到一个类(worker)中,利用多态管理不同职工种类 职工的属性为:职工编号、职工姓名、职工所在部门编号 职工的行为为:岗位职责信息描述,获取岗位名称 头文件文件夹下 创建文件worker.h 文件并且添加如下代码: ```cpp #pragma once#include#include using namespace std; //职工抽象基类 class Worker{ public: //显示个人信息 virtual void showInfo() = 0; //获取岗位名称 virtual string getDeptName() = 0; int m_Id; //职工编号 string m_Name; //职工姓名 int m_DeptId; //职工所在部门名称编号 }; ``` 6.2 创建普通员工类 普通员工类继承职工抽象类,并重写父类中纯虚函数 在头文件和源文件的文件夹下分别创建employee.h 和 employee.cpp文件 employee.h中代码如下: ```cpp #pragma once #include using namespace std; #include "worker.h" //员工类 class Employee :public Worker{ public: //构造函数 Employee(int id, string name, int dId); //显示个人信息 virtual void showInfo(); //获取职工岗位名称 virtual string getDeptName(); }; ``` employee.cpp中代码如下: ```cpp #include "employee.h"Employee::Employee(int id, string name, int dId){ this->m_Id = id; this->m_Name = name; this->m_DeptId = dId; } void Employee::showInfo(){ cout << "职工编号: " << this->m_Id << " \t职工姓名: " << this->m_Name << " \t岗位:" << this->getDeptName() << " \t岗位职责:完成经理交给的任务" << endl; } string Employee::getDeptName(){ return string("员工"); } ``` 6.3 创建经理类 经理类继承职工抽象类,并重写父类中纯虚函数,和普通员工类似 在头文件和源文件的文件夹下分别创建manager.h 和 manager.cpp文件 manager.h中代码如下: ```cpp #pragma once#include using namespace std; #include "worker.h" //经理类 class Manager :public Worker{ public: Manager(int id, string name, int dId); //显示个人信息 virtual void showInfo(); //获取职工岗位名称 virtual string getDeptName(); }; ``` manager.cpp中代码如下: ```cpp #include "manager.h"Manager::Manager(int id, string name, int dId){ this->m_Id = id; this->m_Name = name; this->m_DeptId = dId; } void Manager::showInfo(){ cout << "职工编号: " << this->m_Id << " \t职工姓名: " << this->m_Name << " \t岗位:" << this->getDeptName() << " \t岗位职责:完成老板交给的任务,并下发任务给员工" << endl; } string Manager::getDeptName(){ return string("经理"); } ``` 6.4 创建老板类 老板类继承职工抽象类,并重写父类中纯虚函数,和普通员工类似 在头文件和源文件的文件夹下分别创建boss.h 和 boss.cpp文件 boss.h中代码如下: ```cpp #pragma once#include using namespace std; #include "worker.h" //老板类 class Boss :public Worker{ public: Boss(int id, string name, int dId); //显示个人信息 virtual void showInfo(); //获取职工岗位名称 virtual string getDeptName(); }; ``` boss.cpp中代码如下: ```cpp #include "boss.h"Boss::Boss(int id, string name, int dId){ this->m_Id = id; this->m_Name = name; this->m_DeptId = dId; } void Boss::showInfo(){ cout << "职工编号: " << this->m_Id << " \t职工姓名: " << this->m_Name << " \t岗位:" << this->getDeptName() << " \t岗位职责:管理公司所有事务" << endl; } string Boss::getDeptName(){ return string("总裁"); } ``` 6.5 测试多态 在职工管理系统.cpp中添加测试函数,并且运行能够产生多态 测试代码如下: ```cpp #include "worker.h"#include "employee.h"#include "manager.h"#include "boss.h"void test(){ Worker * worker = NULL; worker = new Employee(1, "张三", 1); worker->showInfo(); delete worker; worker = new Manager(2, "李四", 2); worker->showInfo(); delete worker; worker = new Boss(3, "王五", 3); worker->showInfo(); delete worker; } ``` 运行效果如图: 测试成功后,测试代码可以注释保留,或者选择删除 7、添加职工 功能描述:批量添加职工,并且保存到文件中 7.1 功能分析 分析: 用户在批量创建时,可能会创建不同种类的职工 如果想将所有不同种类的员工都放入到一个数组中,可以将所有员工的指针维护到一个数组里 如果想在程序中维护这个不定长度的数组,可以将数组创建到堆区,并利用Worker **的指针维护 7.2 功能实现 在WokerManager.h头文件中添加成员属性 代码: ```cpp //记录文件中的人数个数 int m_EmpNum; //员工数组的指针 Worker ** m_EmpArray; ``` 在WorkerManager构造函数中初始化属性 ```cpp WorkerManager::WorkerManager(){ //初始化人数 this->m_EmpNum = 0; //初始化数组指针 this->m_EmpArray = NULL;} ``` 在workerManager.h中添加成员函数 ```cpp //增加职工 void Add_Emp(); ``` workerManager.cpp中实现该函数 ```cpp //增加职工 void WorkerManager::Add_Emp(){ cout << "请输入增加职工数量: " << endl; int addNum = 0; cin >> addNum; if (addNum > 0) { //计算新空间大小 int newSize = this->m_EmpNum + addNum; //开辟新空间 Worker ** newSpace = new Worker*[newSize]; //将原空间下内容存放到新空间下 if (this->m_EmpArray != NULL) { for (int i = 0; i < this->m_EmpNum; i++) { newSpace[i] = this->m_EmpArray[i]; } } //输入新数据 for (int i = 0; i < addNum; i++) { int id; string name; int dSelect; cout << "请输入第 " << i + 1 << " 个新职工编号:" << endl; cin >> id; cout << "请输入第 " << i + 1 << " 个新职工姓名:" << endl; cin >> name; cout << "请选择该职工的岗位:" << endl; cout << "1、普通职工" << endl; cout << "2、经理" << endl; cout << "3、老板" << endl; cin >> dSelect; Worker * worker = NULL; switch (dSelect) { case 1: //普通员工 worker = new Employee(id, name, 1); break; case 2: //经理 worker = new Manager(id, name, 2); break; case 3: //老板 worker = new Boss(id, name, 3); break; default: break; } newSpace[this->m_EmpNum + i] = worker; } //释放原有空间 delete[] this->m_EmpArray; //更改新空间的指向 this->m_EmpArray = newSpace; //更新新的个数 this->m_EmpNum = newSize; //提示信息 cout << "成功添加" << addNum << "名新职工!" << endl; } else { cout << "输入有误" << endl; } system("pause"); system("cls"); } ``` 在WorkerManager.cpp的析构函数中,释放堆区数据 ```cpp WorkerManager::~WorkerManager() { if (this->m_EmpArray != NULL) { delete[] this->m_EmpArray; } } ``` 7.3 测试添加 在main函数分支 1 选项中,调用添加职工接口 效果如图: 至此,添加职工到程序中功能实现完毕 8、文件交互 - 写文件 功能描述:对文件进行读写 在上一个添加功能中,我们只是将所有的数据添加到了内存中,一旦程序结束就无法保存了 因此文件管理类中需要一个与文件进行交互的功能,对于文件进行读写操作 8.1 设定文件路径 首先我们将文件路径,在workerManager.h中添加宏常量,并且包含头文件 fstream ```cpp #include #define FILENAME "empFile.txt" ``` 8.2 成员函数声明 在workerManager.h中类里添加成员函数 void save() ```cpp //保存文件void save(); ``` 8.3 保存文件功能实现 ```cpp void WorkerManager::save(){ ofstream ofs; ofs.open(FILENAME, ios::out); for (int i = 0; i < this->m_EmpNum; i++) { ofs << this->m_EmpArray[i]->m_Id << " " << this->m_EmpArray[i]->m_Name << " " << this->m_EmpArray[i]->m_DeptId << endl; } ofs.close(); } ``` 8.4 保存文件功能测试 在添加职工功能中添加成功后添加保存文件函数 再次运行代码,添加职工 同级目录下多出文件,并且保存了添加的信息 9、文件交互 - 读文件 功能描述:将文件中的内容读取到程序中 虽然我们实现了添加职工后保存到文件的操作,但是每次开始运行程序,并没有将文件中数据读取到程序中 而我们的程序功能中还有清空文件的需求 因此构造函数初始化数据的情况分为三种 第一次使用,文件未创建 文件存在,但是数据被用户清空 文件存在,并且保存职工的所有数据 9.1 文件未创建 在workerManager.h中添加新的成员属性 m_FileIsEmpty标志文件是否为空 ```cpp //标志文件是否为空bool m_FileIsEmpty; ``` 修改WorkerManager.cpp中构造函数代码 ```cpp WorkerManager::WorkerManager(){ ifstream ifs; ifs.open(FILENAME, ios::in); //文件不存在情况 if (!ifs.is_open()) { cout << "文件不存在" << endl; //测试输出 this->m_EmpNum = 0; //初始化人数 this->m_FileIsEmpty = true; //初始化文件为空标志 this->m_EmpArray = NULL; //初始化数组 ifs.close(); //关闭文件 return; } } ``` 删除文件后,测试文件不存在时初始化数据功能 9.2 文件存在且数据为空 在workerManager.cpp中的构造函数追加代码: ```cpp //文件存在,并且没有记录 char ch; ifs >> ch; if (ifs.eof()) { cout << "文件为空!" << endl; this->m_EmpNum = 0; this->m_FileIsEmpty = true; this->m_EmpArray = NULL; ifs.close(); return; } ``` 追加代码位置如图: 将文件创建后清空文件内容,并测试该情况下初始化功能 我们发现文件不存在或者为空清空 m_FileIsEmpty 判断文件是否为空的标志都为真,那何时为假? 成功添加职工后,应该更改文件不为空的标志 在`void WorkerManager::Add_Emp()`成员函数中添加: ```cpp //更新职工不为空标志this->m_FileIsEmpty = false; ``` 9.3 文件存在且保存职工数据 9.3.1 获取记录的职工人数 在workerManager.h中添加成员函数 int get_EmpNum(); ```cpp //统计人数int get_EmpNum(); ``` workerManager.cpp中实现 ```cpp int WorkerManager::get_EmpNum(){ ifstream ifs; ifs.open(FILENAME, ios::in); int id; string name; int dId; int num = 0; while (ifs >> id && ifs >> name && ifs >> dId) { //记录人数 num++; } ifs.close(); return num; } ``` 在workerManager.cpp构造函数中继续追加代码: ```cpp int num = this->get_EmpNum(); cout << "职工个数为:" << num << endl; //测试代码 this->m_EmpNum = num; //更新成员属性 ``` 手动添加一些职工数据,测试获取职工数量函数 9.3.2 初始化数组 根据职工的数据以及职工数据,初始化workerManager中的Worker ** m_EmpArray 指针 在WorkerManager.h中添加成员函数 void init_Emp(); ```cpp //初始化员工void init_Emp(); ``` 在WorkerManager.cpp中实现 ```cpp void WorkerManager::init_Emp(){ ifstream ifs; ifs.open(FILENAME, ios::in); int id; string name; int dId; int index = 0; while (ifs >> id && ifs >> name && ifs >> dId){ Worker * worker = NULL; //根据不同的部门Id创建不同对象 if (dId == 1) // 1普通员工 { worker = new Employee(id, name, dId); } else if (dId == 2) //2经理 { worker = new Manager(id, name, dId); } else //总裁 { worker = new Boss(id, name, dId); } //存放在数组中 this->m_EmpArray[index] = worker; index++; } } ``` 在workerManager.cpp构造函数中追加代码 ```cpp //根据职工数创建数组 this->m_EmpArray = new Worker *[this->m_EmpNum]; //初始化职工 init_Emp(); //测试代码 for (int i = 0; i < m_EmpNum; i++){ cout << "职工号: " << this->m_EmpArray[i]->m_Id<< " 职工姓名: " << this->m_EmpArray[i]->m_Name<< " 部门编号: " << this->m_EmpArray[i]->m_DeptId << endl; } ``` 运行程序,测试从文件中获取的数据 至此初始化数据功能完毕,测试代码可以注释或删除掉! 10、显示职工 功能描述:显示当前所有职工信息 10.1 显示职工函数声明 在workerManager.h中添加成员函数 void Show_Emp(); ```cpp //显示职工 void Show_Emp(); ``` 10.2 显示职工函数实现 在workerManager.cpp中实现成员函数 void Show_Emp(); ```cpp //显示职工 void WorkerManager::Show_Emp(){ if (this->m_FileIsEmpty) { cout << "文件不存在或记录为空!" << endl; } else { for (int i = 0; i < m_EmpNum; i++) { //利用多态调用接口 this->m_EmpArray[i]->showInfo(); } } system("pause"); system("cls"); } ``` 10.3 测试显示职工 在main函数分支 2 选项中,调用显示职工接口 测试时分别测试 文件为空和文件不为空两种情况 测试效果: 测试1-文件不存在或者为空情况 测试2 - 文件存在且有记录情况 测试完毕,至此,显示所有职工信息功能实现 11、删除职工 功能描述:按照职工的编号进行删除职工操作 11.1 删除职工函数声明 在workerManager.h中添加成员函数 void Del_Emp(); ```cpp //删除职工void Del_Emp(); ``` 11.2 职工是否存在函数声明 很多功能都需要用到根据职工是否存在来进行操作如:删除职工、修改职工、查找职工 因此添加该公告函数,以便后续调用 在workerManager.h中添加成员函数 int IsExist(int id); ```cpp //按照职工编号判断职工是否存在,若存在返回职工在数组中位置,不存在返回-1int IsExist(int id); ``` 11.3 职工是否存在函数实现 在workerManager.cpp中实现成员函数 int IsExist(int id); ```cpp int WorkerManager::IsExist(int id){ int index = -1; for (int i = 0; i < this->m_EmpNum; i++){ if (this->m_EmpArray[i]->m_Id == id){ index = i; break; }} return index; } ``` 11.4 删除职工函数实现 在workerManager.cpp中实现成员函数 void Del_Emp(); ```cpp //删除职工 void WorkerManager::Del_Emp(){ if (this->m_FileIsEmpty) { cout << "文件不存在或记录为空!" << endl; } else { //按职工编号删除 cout << "请输入想要删除的职工号:" << endl; int id = 0; cin >> id; int index = this->IsExist(id); if (index != -1) //说明index上位置数据需要删除 { for (int i = index; i < this->m_EmpNum - 1; i++) { this->m_EmpArray[i] = this->m_EmpArray[i + 1]; } this->m_EmpNum--; this->save(); //删除后数据同步到文件中 cout << "删除成功!" << endl; } else { cout << "删除失败,未找到该职工" << endl; } } system("pause"); system("cls"); } ``` 11.5 测试删除职工 在main函数分支 3 选项中,调用删除职工接口 测试1 - 删除不存在职工情况 测试2 - 删除存在的职工情况 删除成功提示图: 再次显示所有职工信息,确保已经删除 查看文件中信息,再次核实员工已被完全删除 至此,删除职工功能实现完毕! 12、修改职工 功能描述:能够按照职工的编号对职工信息进行修改并保存 12.1 修改职工函数声明 在workerManager.h中添加成员函数 void Mod_Emp(); ```cpp //修改职工void Mod_Emp(); ``` 12.2 修改职工函数实现 在workerManager.cpp中实现成员函数 void Mod_Emp(); ```cpp //修改职工 void WorkerManager::Mod_Emp(){ if (this->m_FileIsEmpty) { cout << "文件不存在或记录为空!" << endl; } else { cout << "请输入修改职工的编号:" << endl; int id; cin >> id; int ret = this->IsExist(id); if (ret != -1) { //查找到编号的职工 delete this->m_EmpArray[ret]; int newId = 0; string newName = ""; int dSelect = 0; cout << "查到: " << id << "号职工,请输入新职工号: " << endl; cin >> newId; cout << "请输入新姓名: " << endl; cin >> newName; cout << "请输入岗位: " << endl; cout << "1、普通职工" << endl; cout << "2、经理" << endl; cout << "3、老板" << endl; cin >> dSelect; Worker * worker = NULL; switch (dSelect) { case 1: worker = new Employee(newId, newName, dSelect); break; case 2: worker = new Manager(newId, newName, dSelect); break; case 3: worker = new Boss(newId, newName, dSelect); break; default: break; } //更改数据 到数组中 this->m_EmpArray[ret]= worker; cout << "修改成功!" << endl; //保存到文件中 this->save(); } else { cout << "修改失败,查无此人" << endl; } } //按任意键 清屏 system("pause"); system("cls"); } ``` 12.3 测试修改职工 在main函数分支 4 选项中,调用修改职工接口 测试1 - 修改不存在职工情况 测试2 - 修改存在职工情况,例如将职工 "李四" 改为 "赵四" 修改后再次查看所有职工信息,并确认修改成功 再次确认文件中信息也同步更新 至此,修改职工功能已实现! 13、查找职工 功能描述:提供两种查找职工方式,一种按照职工编号,一种按照职工姓名 13.1 查找职工函数声明 在workerManager.h中添加成员函数 void Find_Emp(); ```cpp //查找职工void Find_Emp(); ``` 13.2 查找职工函数实现 在workerManager.cpp中实现成员函数 void Find_Emp(); ```cpp //查找职工 void WorkerManager::Find_Emp(){ if (this->m_FileIsEmpty) { cout << "文件不存在或记录为空!" << endl; } else { cout << "请输入查找的方式:" << endl; cout << "1、按职工编号查找" << endl; cout << "2、按姓名查找" << endl; int select = 0; cin >> select; if (select == 1) //按职工号查找 { int id; cout << "请输入查找的职工编号:" << endl; cin >> id; int ret = IsExist(id); if (ret != -1) { cout << "查找成功!该职工信息如下:" << endl; this->m_EmpArray[ret]->showInfo(); } else { cout << "查找失败,查无此人" << endl; } } else if (select == 2) //按姓名查找 { string name; cout << "请输入查找的姓名:" << endl; cin >> name; bool flag = false; //查找到的标志 for (int i = 0; i < m_EmpNum; i++) { if (m_EmpArray[i]->m_Name == name) { cout << "查找成功,职工编号为:" << m_EmpArray[i]->m_Id << " 号的信息如下:" << endl; flag = true; this->m_EmpArray[i]->showInfo(); } } if (flag == false) { //查无此人 cout << "查找失败,查无此人" << endl; } } else { cout << "输入选项有误" << endl; } } system("pause"); system("cls"); } ``` 13.3 测试查找职工 在main函数分支 5 选项中,调用查找职工接口 测试1 - 按照职工编号查找 - 查找不存在职工 测试2 - 按照职工编号查找 - 查找存在职工 测试3 - 按照职工姓名查找 - 查找不存在职工 测试4 - 按照职工姓名查找 - 查找存在职工(如果出现重名,也一并显示,在文件中可以添加重名职工) 例如 添加两个王五的职工,然后按照姓名查找王五 至此,查找职工功能实现完毕! 14、排序 功能描述:按照职工编号进行排序,排序的顺序由用户指定 14.1 排序函数声明 在workerManager.h中添加成员函数 void Sort_Emp(); ```cpp //排序职工 void Sort_Emp(); ``` 14.2 排序函数实现 在workerManager.cpp中实现成员函数 void Sort_Emp(); ```cpp //排序职工 void WorkerManager::Sort_Emp(){ if (this->m_FileIsEmpty) { cout << "文件不存在或记录为空!" << endl; system("pause"); system("cls"); } else { cout << "请选择排序方式: " << endl; cout << "1、按职工号进行升序" << endl; cout << "2、按职工号进行降序" << endl; int select = 0; cin >> select; for (int i = 0; i < m_EmpNum; i++) { int minOrMax = i; for (int j = i + 1; j < m_EmpNum; j++) { if (select == 1) //升序 { if (m_EmpArray[minOrMax]->m_Id > m_EmpArray[j]->m_Id) { minOrMax = j; } } else //降序 { if (m_EmpArray[minOrMax]->m_Id < m_EmpArray[j]->m_Id) { minOrMax = j; } } } if (i != minOrMax) { Worker * temp = m_EmpArray[i]; m_EmpArray[i] = m_EmpArray[minOrMax]; m_EmpArray[minOrMax] = temp; } } cout << "排序成功,排序后结果为:" << endl; this->save(); this->Show_Emp(); } } ``` 14.3 测试排序功能 在main函数分支 6 选项中,调用排序职工接口 测试: 首先我们添加一些职工,序号是无序的,例如: 测试 - 升序排序 文件同步更新 测试 - 降序排序 文件同步更新 至此,职工按照编号排序的功能实现完毕! 15、清空文件 功能描述:将文件中记录数据清空 15.1 清空函数声明 在workerManager.h中添加成员函数 void Clean_File(); ```cpp //清空文件 void Clean_File(); ``` 15.2 清空函数实现 在workerManager.cpp中实现员函数 void Clean_File(); ```cpp //清空文件 void WorkerManager::Clean_File(){ cout << "确认清空?" << endl; cout << "1、确认" << endl; cout << "2、返回" << endl; int select = 0; cin >> select; if (select == 1) { //打开模式 ios::trunc 如果存在删除文件并重新创建 ofstream ofs(FILENAME, ios::trunc); ofs.close(); if (this->m_EmpArray != NULL) { for (int i = 0; i < this->m_EmpNum; i++) { if (this->m_EmpArray[i] != NULL) { delete this->m_EmpArray[i]; } } this->m_EmpNum = 0; delete[] this->m_EmpArray; this->m_EmpArray = NULL; this->m_FileIsEmpty = true; } cout << "清空成功!" << endl; } system("pause"); system("cls"); } ``` 15.3 测试清空文件 在main函数分支 7 选项中,调用清空文件接口 测试:确认清空文件 再次查看文件中数据,记录已为空 打开文件,里面数据已确保清空,该功能需要慎用! 随着清空文件功能实现,本案例制作完毕 ## 附件 > 📄 C++.docx(原 word 笔记):[[C++.docx|C++.docx]] --- ⬅️ [[02-C++ 基础|C++ 基础]] 🏠 [[00-编程语言|00-编程语言]] ➡️ [[04-C++ 泛型编程|C++ 泛型编程]]